//
//  tdl/net/socket.h
//  Created by tommy on 13/04/25
//

#ifndef tdl_net_socket_h
#define tdl_net_socket_h
extern "C" {
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
} // extern "C"
#include "tdltypes.h"
#include "tdlexcept.h"
#include "net/btypes.h"
using namespace tdl;

namespace tdl {
///////////////
struct socket
{
protected:
	int		m_fd;
	int		m_protocol;
	tdl::host_addr	m_local_ha;

public:
	socket() : m_fd(-1), m_protocol(-1), m_local_ha() {}

	explicit socket(int fd, int protocol) : m_fd(fd), m_protocol(protocol) {}

	virtual ~socket() {}		// SHOULD NOT call close() in this deconstructor
	
	void bind(const tdl::host_addr& ha) throw(socket_ex)
	{
		int r = ::bind(m_fd, ha.get_addr(), sizeof(struct sockaddr));
		if(r < 0)
			THROW2(socket_ex, TRACE_INFO() << "ha=" << ha);
			
		m_local_ha = ha;
	}
	
	int get_fd() const { return m_fd; }
	
	int get_protocol() const { return m_protocol; }
	
	tdl::host_addr get_ha() const { return m_local_ha; }

	void set_nonblock()
	{
		int flag = ::fcntl(m_fd, F_GETFL, 0);
		::fcntl(m_fd, F_SETFL, flag | O_NDELAY | O_NONBLOCK);
	}
	
	// FIXME
	void set_timeout()
	{
	}
	
	// FIXME
	void set_reuse_addr()
	{
		// ::setsockopt(m_fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags))
	}

	void close()
	{
		if(m_fd != -1)
		{
			::close(m_fd);
			m_fd = -1;
		}
	}
};

/////////////////////
// udp
struct udp_socket : public socket
{
public:
	udp_socket() {}
	explicit udp_socket(int fd) : socket(fd, SOCK_DGRAM) {}
	virtual ~udp_socket() {}

	static udp_socket create(bool non_block = true) throw(socket_ex)
	{
		int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
		if(fd < 0)
			THROW2(socket_ex, TRACE_INFO());

		udp_socket s(fd);

		if(non_block)
			s.set_nonblock();

		return s;
	}
	
	void sendto(const char* buf, size_t buflen, const host_addr& ha)
	{
		::sendto(m_fd, buf, buflen, 0, ha.get_addr(), sizeof(sockaddr_in));
	}
	
	ssize_t recvfrom(char* buf, size_t buflen, host_addr& cli_ha, int flags = 0) throw(socket_ex)
	{
		while(1)
		{
			struct sockaddr sa;
			socklen_t sl = sizeof(struct sockaddr);
			ssize_t r = ::recvfrom(m_fd, buf, buflen, flags, &sa, &sl);
			if(r < 0)
			{
				if(errno == EINTR)
					continue;
				else if(errno == EAGAIN)
					return -1 * EAGAIN;
				else
					THROW2(socket_ex, TRACE_INFO());
			}

			cli_ha = sa;
			return r;
		}

		return 0;
	}
};

////////////////////
// tcp
struct tcp_socket : public socket
{	
public:
	explicit tcp_socket(int fd, bool non_block = true) : socket(fd, SOCK_STREAM)
	{
		set_nonblock();
	}

	virtual ~tcp_socket() {}
	
	void send() throw(socket_ex)
	{
	}
	
	ssize_t recv(void* buffer, size_t length, int flags = 0) throw(socket_ex)
	{
		while(1)
		{
			ssize_t n = ::recv(m_fd, buffer, length, flags);
			if(n < 0)
			{
				int e = errno;
				switch(e)
				{
					case EINTR:
						continue;
					case EAGAIN:
					case ETIMEDOUT:
					case ECONNRESET:
						return -1 * e;
					default:
						THROW2(socket_ex, TRACE_INFO());
				}
			}

			return n;
		}

		return 0;
	}
};

struct active_tcp_socket : public tcp_socket
{
protected:
	tdl::host_addr		m_remote_ha;
	
public:
	explicit active_tcp_socket(int fd, bool non_block) : tcp_socket(fd, non_block) {}

	static active_tcp_socket create(bool non_block = true) throw(socket_ex)
	{
		int fd = ::socket(AF_INET, SOCK_STREAM, 0);
		if(fd < 0)
			THROW2(socket_ex, TRACE_INFO());

		return active_tcp_socket(fd, non_block);
	}

	int connect(const tdl::host_addr& ha) throw(socket_ex)
	{
	retry:
		int r = ::connect(m_fd, m_remote_ha.get_addr(), sizeof(struct sockaddr));
		if(r < 0)
		{
			int e = errno;
			// FIXME
			switch(e)
			{
			case EINTR:
			case EAGAIN:
				goto retry;
			case EINPROGRESS:
			case EALREADY:
			case ECONNREFUSED:
			case ETIMEDOUT:
				return e;
			default:
				THROW2(socket_ex, TRACE_INFO() << "ha=" << ha);
			}
		}

		m_remote_ha = ha;

		return 0;
	}
};

struct passive_tcp_socket : public tcp_socket
{
public:
	explicit passive_tcp_socket(int fd, bool non_block) : tcp_socket(fd, non_block) {}

	static passive_tcp_socket create(const tdl::host_addr& ha, bool non_block = true) throw(socket_ex)
	{
		int fd = ::socket(AF_INET, SOCK_STREAM, 0);
		if(fd < 0)
			THROW2(socket_ex, TRACE_INFO());

		passive_tcp_socket s(fd, non_block);

		s.bind(ha);

		if(::listen(s.get_fd(), TDL_TCP_SOCKET_MAX_BACKLOG))
			THROW2(socket_ex, TRACE_INFO());
		
		return s;
	}

	bool operator<(const passive_tcp_socket& r) const
	{ return get_fd() < r.get_fd(); }


	std::pair<tcp_socket, host_addr> accept(bool non_block = true) throw(socket_ex)
	{
		tdl::host_addr peer_ha;
	retry:
		socklen_t socklen = sizeof(struct sockaddr);
		int fd = ::accept(m_fd, peer_ha.get_addr(), &socklen);
		if(fd < 0)
		{
			if(errno == EAGAIN || errno == EINTR)
				goto retry;
			else
				THROW2(socket_ex, TRACE_INFO());
		}
		
		return std::pair<tcp_socket, host_addr>(tcp_socket(fd, non_block), peer_ha);
	}
};

namespace hasel_detail {
///////////////////////////////
template <typename __key> struct hasel_hash;
template <> struct hasel_hash<uint32_t>
{
	pos_type operator()(uint32_t key, const std::vector<host_addr>& vha)
	{
		return (vha.size() == 0) ? -1 : (key % vha.size());
	}
};
template <> struct hasel_hash<uint64_t>
{
	pos_type operator()(uint64_t key, const std::vector<host_addr>& vha)
	{
		return (vha.size() == 0) ? -1 : (pos_type)(key % vha.size());
	}
};
template <> struct hasel_hash<std::string>
{
	pos_type operator()(const std::string& key, const std::vector<host_addr>& vha)
	{
		return -1;		// not implemented yet
	}
};

template <typename __key> struct hasel_poll
{
	uint32_t	idx;
public:
	hasel_poll() : idx(0) {}
	pos_type operator()(const __key& dummy, const std::vector<host_addr>& vha)
	{
		return idx++ % vha.size();
	}
};

////////////////
}	// namespace host_selector_detail

template <typename __key, template <typename> class __hasel = hasel_detail::hasel_hash>
class udp_sender : public udp_socket
{
	typedef __key		key_type;
	typedef __hasel<key_type>	hasel_type;

protected:
	hasel_type		m_hasel;

	std::vector<host_addr>		m_vha;

public:
	void add_dest_host(const host_addr& ha)
	{
		m_vha.push_back(ha);
	}

	void sendto(const key_type& key, const char* buf, size_t buflen, host_addr* pha = NULL)
	{
		pos_type pos = m_hasel(key, m_vha);
		host_addr& ha = m_vha[pos];
		sendto(buf, buflen, ha);
		if(pha)
			*pha = ha;
	}

	void broadcast(const char* buf, size_t buflen)
	{
		for(int i = 0; i < m_vha.size(); ++i)
		{
			host_addr& ha = m_vha[i];
			sendto(buf, buflen, ha);
		}
	}
};

///////////////
}	// namespace tdl

#endif

